﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Peppers.Win32;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32;

namespace System.Peppers.ShellExtensions
{
    /// <summary>
    /// Base class for creating IContextMenu handlers
    /// </summary>
    public abstract class ContextMenuBase : IContextMenu, IShellExtInit
    {
        #region Static
        private const string REG_SHELL_EXT = @"Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved",
            REG_SHELL_ASSOC = @"\shellex\ContextMenuHandlers\";
        #endregion

        #region Private

        private IntPtr _hDrop = IntPtr.Zero;

        #endregion

        #region Internal

        internal Dictionary<uint, ContextMenuItem> Commands
        {
            get;
            private set;
        }

        #endregion

        #region Public

        /// <summary>
        /// Subscribe to this in derived classes to create menu items and add them to the Items property
        /// </summary>
        public event EventHandler ContextMenuQueried;

        /// <summary>
        /// Constructor, creates list objects
        /// </summary>
        public ContextMenuBase()
        {
            Items = new List<ContextMenuItemBase>();
            Commands = new Dictionary<uint, ContextMenuItem>();
        }

        /// <summary>
        /// Selected files that were passed in by windows
        /// *NOTE: not set until OnContextMenu is called
        /// </summary>
        public ReadOnlyCollection<string> Files
        {
            get;
            private set;
        }

        /// <summary>
        /// Items presented in the ContextMenu
        /// </summary>
        public List<ContextMenuItemBase> Items
        {
            get;
            private set;
        }
        #endregion

        #region IContextMenu Members

        uint IContextMenu.QueryContextMenu(IntPtr hMenu, uint iMenu, uint idCmdFirst, uint idCmdLast, CMF uFlags)
        {
            uint id = idCmdFirst + 1;
            if ((uFlags & CMF.CMF_DEFAULTONLY) == 0)
            {
                uint fileCount = Shell32.DragQueryFile(_hDrop, 0xffffffff, null, 0);
                List<string> files = new List<string>((int)fileCount);
                for (uint i = 0; i < fileCount; i++)
                {
                    StringBuilder builder = new StringBuilder(1024);
                    Shell32.DragQueryFile(_hDrop, i, builder, builder.Capacity + 1);
                    files.Add(builder.ToString());
                }
                Files = new ReadOnlyCollection<string>(files);

                if (ContextMenuQueried != null)
                {
                    try
                    {
                        ContextMenuQueried(this, EventArgs.Empty);
                    }
                    catch
                    {
                        //We do this in case someone else's code throws an exception
                        //We wouldn't want explorer to crash
                    }
                }

                for (uint i = 0; i < Items.Count; i++)
                {
                    MENUITEMINFO info = Items[(int)i].BuildStruct(ref idCmdFirst, this);
                    User32.InsertMenuItem(hMenu, i, true, ref info);
                }
            }

            //Return our highest command
            return (uint)idCmdFirst;
        }

        uint IContextMenu.InvokeCommand(IntPtr pici)
        {
            CMINVOKECOMMANDINFO ici = (CMINVOKECOMMANDINFO)Marshal.PtrToStructure(pici, typeof(CMINVOKECOMMANDINFO));
            uint command = (uint)ici.verb - 1;
            ContextMenuItem item = null;
            if (Commands.TryGetValue(command, out item))
            {
                try
                {
                    item.OnClick();
                    return (uint)HRESULT.S_OK;
                }
                catch
                {
                    //Same here, we don't want someone else to make explorer crash
                    return (uint)HRESULT.E_FAIL;
                }
            }
            return (uint)HRESULT.E_FAIL;
        }

        uint IContextMenu.GetCommandString(UIntPtr idcmd, GCS uFlags, IntPtr pwReserved, StringBuilder pszName, uint cchMax)
        {
            return (uint)HRESULT.S_OK;
        }

        #endregion

        #region IShellExtInit Members

        uint IShellExtInit.Initialize(IntPtr pidlFolder, IDataObject pdtobj, IntPtr hkeyProgID)
        {
            FORMATETC fmt = new FORMATETC();
            fmt.cfFormat = CLIPFORMAT.CF_HDROP;
            fmt.ptd = IntPtr.Zero;
            fmt.dwAspect = DVASPECT.DVASPECT_CONTENT;
            fmt.lindex = -1;
            fmt.tymed = TYMED.TYMED_HGLOBAL;
            STGMEDIUM medium = new STGMEDIUM();
            pdtobj.GetData(ref fmt, ref medium);
            _hDrop = medium.hGlobal;
            return (uint)HRESULT.S_OK;
        }

        #endregion

        #region COM Registration
        /// <summary>
        /// Call this from your COM registration fuction in a derived class
        /// </summary>
        /// <param name="type">Type of derived class</param>
        protected static void RegisterExtension(Type type)
        {
            GuidAttribute guidAttr = null;
            ShellExtensionAttribute extAttr = null;
            ShellExtensionHelper.ReflectAttributes(type, out guidAttr, out extAttr);

            if (guidAttr == null || extAttr == null || string.IsNullOrEmpty(extAttr.Name) || extAttr.Data == null)
            {
                throw new ArgumentException("Invalid attributes on type.");
            }
            Guid id = new Guid(guidAttr.Value);
            if (id == Guid.Empty)
            {
                throw new ArgumentException("Invalid attributes on type.");
            }

            try
            {
                string guid = id.ToString("B").ToUpper();

                using (RegistryKey key = Registry.LocalMachine.OpenSubKey(REG_SHELL_EXT, true))
                {
                    key.SetValue(guid, extAttr.Name);
                }

                foreach (string ext in extAttr.Data)
                {
                    using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(ext + REG_SHELL_ASSOC + extAttr.Name))
                    {
                        key.SetValue(string.Empty, guid);
                    }
                }
            }
            catch (Exception e)
            {
                Console.Error.WriteLine(e.ToString());
            }
        }

        /// <summary>
        /// Call this from your COM un-registration function in a derived class
        /// </summary>
        /// <param name="type">Type of derived class</param>
        protected static void UnregisterExtension(Type type)
        {
            GuidAttribute guidAttr = null;
            ShellExtensionAttribute extAttr = null;
            ShellExtensionHelper.ReflectAttributes(type, out guidAttr, out extAttr);

            if (guidAttr == null || extAttr == null || string.IsNullOrEmpty(extAttr.Name) || extAttr.Data == null)
            {
                throw new ArgumentException("Invalid attributes on type.");
            }
            Guid id = new Guid(guidAttr.Value);
            if (id == Guid.Empty)
            {
                throw new ArgumentException("Invalid attributes on type.");
            }

            try
            {
                string guid = id.ToString("B").ToUpper();

                using (RegistryKey key = Registry.LocalMachine.OpenSubKey(REG_SHELL_EXT, true))
                {
                    key.DeleteValue(guid);
                }
            }
            catch (Exception exc)
            {
                Console.Error.WriteLine(exc.ToString());
            }

            foreach (string ext in extAttr.Data)
            {
                try
                {
                    using (RegistryKey key = Registry.ClassesRoot)
                    {
                        key.DeleteSubKey(ext + REG_SHELL_ASSOC + extAttr.Name);
                    }
                }
                catch (Exception exc)
                {
                    Console.Error.WriteLine(exc.ToString());
                }
            }
        }

        #endregion
    }
}
